/**
 * @description: js简单路由实现
 * @author: 宫晓辉
 * @update: 20190319
 * @ps: 当前只允许一级路由,不允许子路由
 * @example: 使用时先注册Router.init({el:'#route',pagePath:'src',routes:['/','/blue','/green']})
 * 			 1.el是将路由的DOM渲染到哪个元素里
 *		 	 2.tempPath是路由模板文件存放的文件夹路径，是相对于根目录的路径，如src、pages
 * 				这个路径下还有路由文件夹，最终形成的路由文件路径可能是：根目录/src/home/home.html
 * 			 3.路由模板文件名必须和路由名一致，必须是html文件，如home.html
 * 			 4.css样式必须写到模板文件同目录下的同名css文档中，如/src/home/home.css,且必须有这个文件
 * 			 5.模板引擎引用数据格式(使用with语句)：{{name}},{{name.a}},{{arr.length}},{{a.arr.length}},{{a+b-c/d*e}},{{a?b:c}}
 * 			 6.模板引擎使用JS语句格式：<%if(this.aaa){%> <%}%>,格式中支持一些语句，如if|for|else|switch|case|break,
 * 				不使用{{}}格式是因为JS语句中会经常用到花括号作用域，会导致混淆。注意：<%%>中的变量使用this调用如<%if(this.aaa){%>
 * 			 7.所有数据保存在_data，可以使用Router.data({})设置，
 * 				每个路由的数据都会在_data里不同的对象中，不混淆。
 * 				首页的数据在_data一级，使用_data[路由名]可以访问其他路由的数据
 * 			 8.手动切换路由this.goto(path)
 * 			 9.可使用this.setData({name:'22321211212'})动态（异步）更改data数据
 */	
 
 /*BUG1   <%%> 语句内的dom中的{{}}数据无法渲染，因为以被渲染为null*/
 /*BUG2   DOM元素动态获取需要用jq的on方法*/
 /*BUG3   行内事件无法直接调用Router中的方法，必须用Router.aaa()调用*/
 
(function(window) {
	var location = window.location,document = window.document
	var Router = function() {
		this.el = '#route'
		this.pagePath = 'src'
		this.routes = []	//注册的所有路由(路径path带/)
		this.route = {	//当前路由信息
			name:'',
			path:'',
			query:{},
			to:{	//去往
				name:'',
				path:'',
				query:{},
			},
			from:{	//来自
				name:'',
				path:'',
				query:{},
			}
		}
		this._data = {}	//所有传入的数据保存在这里
		this.beforeFn = null
		this.afterFn = null
		this.asyncJsNode = null //路由js节点
		this.HTML = ''	//当前页面的innerHTML内容(未渲染)
	}
	Router.prototype = {
		init:function(initData) {	//路由注册，监听url变化
			this.el = initData&&initData.el?initData.el:'#route'	//默认渲染到#route元素
			this.pagePath = initData&&initData.pagePath?initData.pagePath:'src' //默认路由模板文件放置在本js文件夹父路径的src文件夹中
			this.routes = initData&&initData.routes?initData.routes:[]	//配置路由信息(路径path带/)
			if(this.routes.length==0){
				throw new Error('当前暂无注册任何路由,请重新配置！')
				return;
			}
			var _this = this
			window.addEventListener('load',function(){ //监听页面加载匹配路由(刷新时,hash切换不算刷新)
				_this.curRoute()	//重置路由参数，加载路由
			})
			window.addEventListener('hashchange',function(){ //监听路由切换(切换时)
				_this.curRoute()	//重置路由参数，加载路由
			})
		},
		Set:function(obj){ //路由输入钩子函数，在各路由的js脚本中使用其进行配置
			for(var key in obj){
				if(key=='data'){
					if(this.route.name==''){
						Object.assign(this._data,obj.data)
					}else{
						if(this._data[this.route.name]){
							Object.assign(this._data[this.route.name],obj.data)
						}else{
							this._data[this.route.name] = obj.data
						}
					}
					this[key] = obj[key]	//可使用this.data.aaa访问aaa数据
				}else if(key=='beforeOnload'||key=='onload'){
					this[key](obj[key])
				}else{
					this[key] = obj[key]
				}
			}
		},
		setData:function(obj){	
			//所有数据保存在这里，可以使用Router.setData({})设置
			//每个路由的数据都会在_data里不同的对象中，不混淆。首页的数据在_data一级
			//各路由之间可以相互访问数据，切换路由后不会清空其数据，使用Object.assign合并对象
			if(typeof obj == 'object'){
				if(this.route.name==''){
					Object.assign(this._data,obj)
				}else{
					if(this._data[this.route.name]){
						Object.assign(this._data[this.route.name],obj)
					}else{
						this._data[this.route.name] = obj
					}
				}
				//每次一执行此方法，说明修改了数据，就要再渲染一次页面
				var _this = this
				if(_this.route.path=='/'){	//首页
					var domTxt = _this.hsdom(_this.HTML,_this._data)
					document.querySelector(_this.el).innerHTML = domTxt
				}else{
					//数据渲染
					var domTxt = _this.hsdom(_this.HTML,_this._data[_this.route.name])
					//传入css样式文件
					domTxt+='<link rel="stylesheet" type="text/css" href="'+_this.pagePath+_this.route.path+_this.route.path+'.css">'
					document.querySelector(_this.el).innerHTML = domTxt
				}
			}else{
				throw new Error('请传入json对象格式的数据！')
			}
		},	
		curRoute:function(){	//当前路由
			this.route.from = {
				name:this.route.name,
				path:this.route.path,
				query:this.route.query
			}
			var name = location.hash.slice(2) || '',
				path = location.hash.slice(1) || '/',
				query = {},
				params = location.search[1]	? location.search.slice(1).split("&") : []	//参数内容
			for(var i=0;i<params.length;i++){	//将query转换为json
				var item = params[i].split("=");
				query[item[0]] = item[1]
			}
			this.route.to = {
				name:name,
				path:path,
				query:query	
			}
			this.route.name = name
			this.route.path = path
			this.route.query = query
			if(this.routes.indexOf(this.route.path)<0){	//404默认跳到首页
				throw new Error(this.route.path+'没有注册，请添加！')
				this.goto('/')
				return;
			}
			//加载路由
			this.load()	
		},
		goto:function(path) {
			if(this.routes.indexOf(path)<0){
				throw new Error('需要跳转的路由没有注册，请添加！')
				return;
			}
			location.hash = path	//跳转
			this.curRoute()	//重置路由参数，加载路由
		},
		beforeOnload:function(callback) {	//路由加载前钩子函数
			if(typeof callback == 'function'){
				this.beforeFn = callback
			}else{
				throw new Error('路由切换前的钩子函数不正确')
			}
		},
		onload:function(callback) {	//路由加载完成后钩子函数
			if(typeof callback == 'function'){
				this.afterFn = callback
			}else{
				throw new Error('路由加载完成的钩子函数不正确')
			}
		},
		load:function() {	//异步加载路由文件，渲染到页面
			if(this.beforeFn!=null) this.beforeFn()	//加载之前执行操作
			var xhr = new XMLHttpRequest() , _this = this ,
				_url = _this.route.path=='/'?'index.html':_this.pagePath+_this.route.path+_this.route.path+'.html'
			xhr.open('GET',_url,true)
			xhr.send()
			xhr.onreadystatechange = function(){
				if(xhr.readyState==4){
					if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
						_this.HTML = xhr.responseText		//转存未渲染的DOM内容
						//执行路由的js文件
						if(_this.route.path=='/'){	//首页
							//数据渲染
							var domTxt = _this.hsdom(xhr.responseText,_this._data)
							document.querySelector(_this.el).innerHTML = domTxt
						}else{
							_this.asyncJs(_this.pagePath+_this.route.path+_this.route.path+'.js',function(){
								//数据渲染
								var domTxt = _this.hsdom(xhr.responseText,_this._data[_this.route.name])
								//传入css样式文件
								domTxt+='<link rel="stylesheet" type="text/css" href="'+_this.pagePath+_this.route.path+_this.route.path+'.css">'
								document.querySelector(_this.el).innerHTML = domTxt
								if(_this.afterFn!=null) _this.afterFn()	//加载之后执行操作
							})
						}
					}else{
						if(xhr.status==404){
							_this.goto('/')
							throw new Error('404错误页面，默认返回首页')
						}else{
							throw new Error(xhr.status+':服务器请求失败！')
						}
					}
				}
			}
		},
		//路由异步懒加载js文件
		asyncJs:function(url,callback){
			if(url==null){
				return
			}
			var body = document.getElementsByTagName('body')[0]
			if(body.contains(this.asyncJsNode)){	//判断body是否包含原js节点
				body.removeChild(this.asyncJsNode)	//已存在的话移除，防止多个路由的js产生冲突
			}
	   		var _this = this , scriptEl= document.createElement('script')
           	scriptEl.type= 'text/javascript';
           	scriptEl.src= url;
           	scriptEl.async = true;
           	body.appendChild(scriptEl)
           	scriptEl.onload= function(){
           		_this.asyncJsNode = scriptEl
           		callback()	//执行回调
           	}
		},
		hsdom:function(domStr,data) {	//模板字符串功能，使用{{}}实现数据渲染和h-for属性实现DOM标签循环
			if(data && typeof data != "object"){
				throw new Error('路由的data数据必须是JSON对象格式！')
				return;
			}else if(this.route.path!='/' && (domStr.indexOf('<html>')>0||domStr.indexOf('<body>')>0||domStr.indexOf('<head>')>0)){
				throw new Error(this.route.name+'.html文件内只能含有基本标签，不能含有<html><body><head>等标签')
				return;
			}
			if(this.route.path=='/'){	//如果是首页，直接返回原dom
				var el = document.createElement('DIV')
				el.innerHTML = domStr
				this.afterFn = null	//置空加载完成函数，因为首页不算是路由
				return el.querySelector(this.el).innerHTML
			}
			
			/*测试其他方法*/
			
			/*测试其他方法END*/
			
			
			/*普通数据渲染,使用{{}}标记*/
			var noteReg = /\<!--(.*)?-->/g		//正则排除所有的注释
			var reg = /\{\{([^\{\}]+)?\}\}/g		//正则找到所有引用的变量,排除{},以便排除匹配到类似{{name}}{{age}}的情况
			var jsreg = /<%([^%>]+)?%>/g	//正则找到使用<%%>定义的语句，以便使用各种语句渲染DOM
			/*考虑到<%%>js语句与其之间的dom代码不能一起运行，dom需要是字符串的情况，所以这里先预制一个数组，用字符串js的格式，以便之后使用join把数组内所有的
				js语句片段合并成字符串，domArr数组内放置的是DOM片段字符串*/
			var codeStr = "var domArr = [];\n" , cursor = 0; //cursor记录渲染的domStr节点位置，cursor-光标
			//去除注释
			domStr = domStr.replace(noteReg,'')
			//替换{{}}中的内容
			while(matchArr = reg.exec(domStr)){	//exec()方法便于找出模板字符串及变量名,while便于循环输出所有
				with(data){	//with方法将data作用域放置到全局作用域中，使得可以直接使用a变量访问data中的a字段值
					//matchArr[0]是原字符串，这里可能有多个相同的调用如：{{name}}{{name}},所以要用正则匹配所有
					var re1 = /\{|\}|\[|\]|\||\?|\(|\)|\+|\.|\^/g	//去除正则中混淆字符
					var match = matchArr[0].replace(re1,"\\$&")	//将混淆字符转义
					var re = new RegExp(match,'g')
					//matchArr[1]是一个字符串，这里要用eval转变为js进行运行
					try{
						domStr = domStr.replace(re,eval(matchArr[1]))
					}catch(err){	//避免DOM中的变量在data中未定义
						domStr = domStr.replace(re,null)
					}
				}
			}
			//执行<%%>语句，可使用if|for|else|switch|case|break等语句
			if(domStr.indexOf('<%')>0){	//放到if语句中，避免没有此语句的时候还执行
				while(matchJsArr = jsreg.exec(domStr)){
					//加载DOM片段（<%%>将其分割，放到domArr中）
					//注意：这里的DOM片段不是所有的，而是<%%>语句执行后的结果，如果存在for或if语句，此push方法可能会执行或不执行或执行多次
					//由于第一段DOM片段不在两个<%%>包裹内，是不参与语句执行的，所以放到最开始执行
					codeStr += 'domArr.push("' + domStr.slice(cursor, matchJsArr.index).replace(/"/g, '\\"') + '");\n';	
					//加载<%%>中的js语句
					codeStr+=matchJsArr[1]+'\n'
					//每次循环重置cursor
					cursor = matchJsArr.index + matchJsArr[0].length;
				}
				//加载最后一段DOM片段（最后一个<%%>语句后面的DOM片段，不参与语句运行）
				codeStr += 'domArr.push("' + domStr.substr(cursor, domStr.length - cursor).replace(/"/g, '\\"') + '");\n';	
				codeStr += 'return domArr.join("");'; 
				domStr = new Function(codeStr.replace(/[\r\t\n]/g, '')).apply(data)	//构建函数，赋值,使用apply方法与data共域
			}
			return domStr
		}
	}
	window.Router = new Router()	//赋值给全局
})(window)
